昨天談到 write skew 和 phantoms ,是 2 種特別難重現的 競爭條件 (race condition) 情況,也就代表無法針對這些情況做測試,這些只有在你衰小要找奇怪的 bug 時才會遇到,這是個老問題了,而這一切的解法也就顯而易見,使用 序列化隔離 (serializable isolation) 。
序列化隔離是最強的隔離等級,儘管 transaction 能並發執行,但它保證了最終的執行結果等同一次只執行一個 transaction,重點是 連續 (serially) 這個字,表示無並發性 (without concurrency),所以它就能避免所有的競爭條件。
序列化隔離的實現方法有 3 種:
(1) 字面上的,連續 (serially) 執行 transactions。
(2) 二階段鎖 (Two-Phase Locking 2PL),主宰數十年的實作方式。
(3) 優化並發控制技術,就像有了序列化能力的快照隔離。
最簡單暴力直覺的實作方法,每一個時間點只會有一個 transaction 執行在單一執行緒上。
為什麼這方法變的可行了呢?歸功於越來越便宜和越來越強的 RAM,讓一切在 RAM 上執行變的可行;再加上 OLTP transaction 都是短小精幹的讀取跟寫入,長時間寫的讀取要用 OLAP + 快照隔離來區分。
但是!這就代表了這個實作方法的吞吐量很吃單一 CPU 核心效能,為了讓單一執行緒用的更有效率,書中建議我們可以將數個 transaction 會做的事合併成一個 預存程序 (stored procedures) 來執行,就是要減少網路 IO,盡量避免 交互式多語句 (interactive multi-statement) transaction,
以下延用 Day 5 圖 7-8 的醫生排班案例。
所以 連續執行 (serial execution) 要可行,最好還是不要違反以下幾點的限制:
主宰了序列化隔離 30 幾年的實作算法,其實我們在 Day 3 - No Dirty Write 小節看到資料庫是怎麼用鎖去避免 Dirty Write,二階段鎖 (Two-Phase Locking) 也是類似的概念,但鎖更強大,當沒有 transaction 正在寫入時,多個 transaction 允許並發讀取同一個物件,一旦有 transaction 想寫入物件,它會:
在二階段鎖中,寫入只會阻檔讀取,反之亦然,這跟快照隔離 (Day 4) 的寫入跟讀取不會互相影響 的核心精神很不同,所以二階段鎖能避免所有的競爭條件寫入、昨天提的更新遺失 (lost update) 和 write skew。
為了實現這個寫入跟讀取互相阻檔的資料庫 全物件鎖,鎖的狀態可以是 共享模式 (shared mode) 或者是 互斥模式 (exclusive mode),該鎖跟隨以下規則:
用這麼多鎖免不了可能會發生 死結 (deadlock),也就是 2 個 transaction 彼此等待互相釋放鎖,幸運的是現在的資料庫會自動偵測死結,然後 abort,之後靠應用程式做重試。
二階段鎖最大的缺點就是效能啦,transaction 的吞吐量和回應時間比起 弱等級隔離 (Weak Isolation Levels) 要糟上許多,其最大的原因就是要等待 互斥模式 鎖的釋放,如果你的 transcation 又執行的稍稍久一點,回應時間就爛掉了,所以 2PL 會有非常不穩定的延遲 (lantency),會有非常慢或非常快的回應時間百分位 (2020 Day 3) 發生,還有剛剛講的死結問題也需要時間解決。
第 (3) 項就明天再講啦!